package org.example.mall.admin.service.manage.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.net.URLEncoder;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import com.amazonaws.services.s3.model.S3Object;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.undertow.util.Headers;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.example.mall.admin.configuration.exception.BaseException;
import org.example.mall.admin.configuration.file.*;
import org.example.mall.admin.mapper.manage.SysFileMapper;
import org.example.mall.admin.model.entity.manage.SysFile;
import org.example.mall.admin.service.manage.SysFileService;
import org.example.mall.admin.util.R;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * <p>
 * 文件管理表 服务实现类
 * </p>
 *
 * @author hzq
 * @since 2022-01-04
 */
@Slf4j
@Service
@AllArgsConstructor
public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements SysFileService {

    private final OssTemplate minioTemplate;
    private final OssProperties ossProperties;
    private final ThreadPoolTaskExecutor applicationTaskExecutor;

    @Override
    public OssProperties ossProperties() {
        return ossProperties;
    }

    @Override
    public OssTemplate minioTemplate() {
        return minioTemplate;
    }

    @Override
    public ThreadPoolTaskExecutor applicationTaskExecutor() {
        return applicationTaskExecutor;
    }

    @Override
    public IPage<SysFile> getPage(Page page, SysFile sysFile) {
        return getBaseMapper().getPage(page, sysFile);
    }

    @Override
    public Boolean moveMaterialFile(Integer treeId, String fileIds) {
        if (treeId == 0) {
            throw new BaseException("不能移动至全部素材");
        }
        Set<SysFile> sysFiles = Stream.of(fileIds.split(StrPool.COMMA))
                .map(fileId -> {
                    SysFile file = new SysFile();
                    file.setId(Long.parseLong(fileId));
                    file.setTreeId(treeId);
                    return file;
                }).collect(Collectors.toSet());
        if (CollUtil.isNotEmpty(sysFiles)) {
            updateBatchById(sysFiles);
        }
        return Boolean.TRUE;
    }

    /**
     * 上传文件
     */
    @Override
    public R<Map<String, String>> uploadFile(String bucketName, MultipartFile file, Integer treeId) {
        String fileName;
        String finalBucketName = extractBucketName(bucketName);
        String originalFileName = file.getOriginalFilename();
        long fileSize = file.getSize();
        try {
            byte[] bytes = file.getBytes();
            String md5Hex = DigestUtils.md5DigestAsHex(bytes);
            SysFile sysFile = getSysFile(md5Hex);
            if (sysFile != null) {
                if (!finalBucketName.equals(sysFile.getClassification())) {
                    sysFile.setClassification(finalBucketName);
                    updateById(sysFile);
                }
                fileName = sysFile.getFileName();
                finalBucketName = sysFile.getBucketName();
            }
            // 上传文件到minio服务器
            else {
                fileName = extractFileName(originalFileName, md5Hex);
                minioTemplate().putObject(finalBucketName, fileName, new ByteArrayInputStream(bytes));
                sysFile = saveSysFileLog(finalBucketName, originalFileName, fileName, fileSize, md5Hex, null, file.getContentType(), treeId);
            }
            Map<String, String> resultMap = new HashMap<>(4);
            resultMap.put("bucketName", finalBucketName);
            resultMap.put("fileName", fileName);
            resultMap.put("url", sysFile.getUrl());
            return R.ofSuccess(resultMap);
        } catch (Exception e) {
            log.error("上传失败", e);
            return R.ofErrorMessage(e.getLocalizedMessage());
        }
    }

    @Override
    public String uploadNetFile(String bucketName, String netUrl, String fileName) {
        Future<String> future = applicationTaskExecutor().submit(SysFileUtil.netFileToMinio(netUrl, this, bucketName, fileName));
        try {
            return future.get();
        } catch (ExecutionException | InterruptedException ignored) {
        }
        return null;
    }

    /**
     * @param bucketName 桶名称
     * @return 桶名
     */
    @Override
    public String extractBucketName(String bucketName) {
        try {
            return BucketEnum.valueOf(bucketName).getBucketName();
        } catch (IllegalArgumentException | NullPointerException ignored) {
            return ossProperties().getBucketName();
        }
    }

    /**
     * 2022/1/6/uuid.png
     *
     * @param originFileNme 原始文件名
     * @param md5Hex        文件唯一值
     * @return 新的文件名
     */
    @Override
    public String extractFileName(String originFileNme, String md5Hex) {
        String expandName = FileUtil.extName(originFileNme);
        LocalDate now = LocalDate.now();
        return now.getYear() + StrPool.SLASH +
                now.getMonthValue() + StrPool.SLASH +
                now.getDayOfMonth() + StrPool.SLASH +
                md5Hex + StrPool.DOT + expandName;
    }

    /**
     * 读取文件
     *
     * @param bucket   桶名称
     * @param fileName 文件名
     * @param response 响应流
     */
    @Override
    public void getFile(String bucket, String fileName, HttpServletResponse response) {
        SysFile sysFile = getByFileName(fileName);
        // todo 判断文件是否存在 是否可被直接访问 如收费软件文件等
        try (S3Object s3Object = minioTemplate().getObject(bucket, fileName)) {
            String originFileName = URLEncoder.DEFAULT.encode(sysFile.getOriginal(), CharsetUtil.CHARSET_UTF_8);
            response.setHeader(Headers.CONTENT_DISPOSITION_STRING, "attachment;filename=" + originFileName);
            response.setContentType("application/octet-stream; charset=UTF-8");
            IoUtil.copy(s3Object.getObjectContent(), response.getOutputStream());
        } catch (Exception e) {
            log.error("文件读取异常: {}", e.getLocalizedMessage());
        }
    }

    /**
     * 删除文件
     *
     * @param id 文件id
     */
    @Override
    @SneakyThrows
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteFile(Long id) {
        SysFile file = getById(id);
        minioTemplate().removeObject(file.getBucketName(), file.getFileName());
        return removeById(id);
    }

    @Override
    public SysFile getSysFile(String md5Hex) {
        SysFile condition = new SysFile();
        condition.setMd5Hex(md5Hex);
        return getOne(new QueryWrapper<>(condition));
    }

    @Override
    public void linkFile(String fileName) {
        editLinkNum(fileName, 1);
    }

    /**
     * 异步绑定
     */
    @Override
    public void asyncLinkFile(Object target) {
        applicationTaskExecutor().execute(() -> syncLinkFile(target));
    }

    /**
     * 同步绑定
     */
    void syncLinkFile(Object target) {
        Set<String> fileNames = SysFileUtil.getFileNames(target);
        for (String fileName : fileNames) {
            linkFile(fileName);
        }
    }

    /**
     * 异步修改绑定
     */
    @Override
    public void asyncEditLinkFile(Object original, Object target) {
        applicationTaskExecutor().execute(() -> {
            syncUnLinkFile(original);
            syncLinkFile(target);
        });
    }

    /**
     * 异步解绑
     */
    @Override
    public void asyncUnLinkFile(Object target) {
        applicationTaskExecutor().execute(() -> syncUnLinkFile(target));
    }

    /**
     * 同步解绑
     */
    void syncUnLinkFile(Object target) {
        Set<String> fileNames = SysFileUtil.getFileNames(target);
        for (String fileName : fileNames) {
            unLinkFile(fileName);
        }
    }

    @Override
    public void unLinkFile(String fileName) {
        editLinkNum(fileName, -1);
    }

    @Override
    public void unLinkFileByUrls(String urls) {
        Set<String> fileNames = SysFileUtil.getFileNames(urls);
        for (String fileName : fileNames) {
            unLinkFile(fileName);
        }
    }

    void editLinkNum(String fileName, int num) {
        if (StrUtil.isNotBlank(fileName)) {
            SysFile sysFile = getByFileName(fileName);
            if (sysFile != null) {
                SysFile file = new SysFile();
                file.setId(sysFile.getId());
                file.setLinkNum(sysFile.getLinkNum() + num);
                updateById(file);
            }
        }
    }

    @Override
    public BaseCallback replaceNetBaseCallback(String text) {
        return SysFileUtil.replaceNetBaseCallback(text, this);
    }

    @Override
    public UrlsCallback replaceNetUrlsCallback(String text) {
        return SysFileUtil.replaceNetUrlsCallback(text, this);
    }

    /**
     * @param fileName 文件名
     * @return 文件信息
     */
    @Override
    public SysFile getByFileName(String fileName) {
        SysFile condition = new SysFile();
        condition.setFileName(fileName);
        return getOne(new QueryWrapper<>(condition));
    }

    /**
     * 文件管理数据记录,收集管理追踪文件
     *
     * @param bucketName     桶名称
     * @param originFileName 原始文件名称
     * @param fileName       唯一文件名称
     * @param fileSize       文件大小
     * @param md5Hex         文件唯一
     * @param netUrl         原图片网络地址
     * @param contentType    文件类型
     * @param treeId         分类id
     */
    @Override
    public SysFile saveSysFileLog(String bucketName, String originFileName, String fileName, long fileSize,
                                  String md5Hex, String netUrl, String contentType, Integer treeId) {
        SysFile sysFile = new SysFile();
        String[] types = contentType.split(StrPool.SLASH);
        sysFile.setBucketName(bucketName);
        sysFile.setOriginal(originFileName);
        sysFile.setFileName(fileName);
        sysFile.setFileSize(fileSize);
        sysFile.setType(contentType);
        sysFile.setMd5Hex(md5Hex);
        sysFile.setNetUrl(netUrl);
        sysFile.setBigType(types[0]);
        sysFile.setSmallType(types[1]);
        sysFile.setUrl(String.format("%s?bucket=%s&fileName=%s", ossProperties().getPrefix(), bucketName, fileName));
        sysFile.setTreeId(treeId == null ? null : treeId == 0 ? null : treeId);
        sysFile.setClassification(bucketName);
        save(sysFile);
        return sysFile;
    }
}
